Mise à jour le 17/11/2021
Static Factory

Static Factory

1. Introduction

FactoryMethod/AbstractFactory/StaticFactory/SimpleFactory : peu importe le nom qu'on donne à ces designs patterns, il faut surtout comprendre que le but d'une classe suffixée Factory est celui-ci : construire et retourner des objets.
Que cette classe Factory étende une AbstractFactory, qu'elle contienne une ou plusieurs méthodes de fabrication, qu'elle soit elle même générée par une autre Factory, l'essentiel est qu'elle crée des instances.
En outre, si le Factory a ce rôle de constructeur, il paraitrait logique que tous les 'new' ne soient présents que dans de telles classes dans tout le code source du projet.

Les méthodes de création des classes suffixées Factory sont justement nommées FactoryMethod. "(create(...)/factory(...)/make(...)")


2. Intention

Fournir une interface permettant de créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes. A la différence de l'AbstractFactory qui définit des Factory ayant plusieurs méthodes de fabrication, la StaticFactory définit des Factory qui n'ont qu'une seule méthode statique de fabrication pour un ensemble d'objets.

🧙‍♂️️Ce que je comprends plutôt de ce qu'est le StaticFactory :
A la différence de l'AbstractFactory, on ne définit ici qu'une seule classe Factory ; étant donné qu'il n'y a qu'une seule classe Factory, il n'est donc pas nécessaire de mettre du code dans une classe abstraite (donc le fichier 'AbstractSomethingFactory.php' n'existerait pas). Le design pattern StaticFactory est un cas particulier de l'AbstractFactory.


3. Exemple concret

Supposons que l'on souhaite afficher à l'écran le nom du repas à prendre en fonction du moment de la journée (par exemple pour une personne qui n'aurait plus toute sa tête). Pour simplifier, on ne considère que trois types de repas : le petit déjeuner, le déjeuner et le diner.

3.1 Code procédural

En procédural, voici ce que l'on pourrait écrire :

index.php
<?php
$currentHour = (int) date('H');

if ($currentHour < 12) {
    echo 'breakfeast';
} elseif ($currentHour < 16) {
    echo 'lunch';
} else {
    echo 'dinner';
}

Ici, il y a un problème fondamental : la logique heure/type de repas n'est pas récupérable en l'état dans les autres fichiers de l'application, il faudra alors dupliquer ce code. Si un nouveau repas est introduit (comme le petit déjeuner), il faudra chercher dans l'ensemble du code cette logique pour la changer.

Une fonction aurait tout sa place ici.
Par exemple :

<?php
    function getMealName($currentHour): string
    {
        if ($currentHour < 12) {
            $mealName = 'breakfeast';
        } elseif ($currentHour < 16) {
            $mealName = 'lunch';
        } else {
            $mealName = 'dinner';
        }

        return $mealName;
    }
    echo getMealName((int) date('H'));

Cette fonction résout le problème principal : la logique heure/type de repas est à un seul endroit.
Là où cela se complique, c'est si l'on souhaite retourner autre chose que le nom du repas, par exemple les horaires de celui-ci, on pourrait éventuellement faire une deuxième fonction comme celle-ci :

    function getMealTime($currentHour): string
    {
        if ($currentHour < 12) {
            $mealTime = '0h - 12h';
        } elseif ($currentHour < 16) {
            $mealTime = '12h - 16h';
        } else {
            $mealTime = '> 16h';
        }

        return $mealTime;
    }
    echo getMealTime((int) date('H'));

Mais cette fonction génère à nouveau un problème de duplication.
Non seulement on duplique à nouveau la logique heure/type de repas mais s'il faut rajouter un type de repas, il faudra modifier les deux fonctions.

La solution ici est de renvoyer une donnée plus complexe qui contiendrait plusieurs informations. Cette donnée pourrait contenir à la fois le nom du repas mais également les horaires de celui-ci. Il existe des solutions pour cela : on peut retourner un tableau, une structure particulière (ex: json/xml/yaml) ou bien un Objet. Le tableau ou la structure pose une difficulté : le client de la fonction doit connaitre le nom des clefs d'accès aux différentes informations. Dans le cas d'un Objet, le client a accès aux méthodes publiques de celui-ci pour accéder à une représentation des différentes informations.

3.2 En POO

3.3 Création des modèles Meal

En POO, on pourrait représenter chaque type de repas de la façon suivante :

MealInterface.php
<?php 

namespace App\Model\Meal;

interface MealInterface
{
    public function getName();
}

AbstractMeal.php
<?php
namespace App\Model\Meal;

abstract class AbstractMeal implements MealInterface
{
    protected string $name;

    public function getName(): string
    {
        return $this->name;
    }
}

Breakfeast.php
<?php
namespace App\Model\Meal;

class Breakfast extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'breakfast';
    }
}

Lunch.php
<?php
namespace App\Model\Meal;

class Lunch extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'lunch';
    }
}

Dinner.php
<?php
namespace App\Model\Meal;

class Dinner extends AbstractMeal implements MealInterface
{
    public function __construct()
    {
        $this->name = 'dinner';
    }
}

3.4 Mise en application sans Factory

Maintenant que les différentes classes sont prêtes, on peut les utiliser :

MealController.php
<?php 
    namespace App\Meal\Controller;

    use App\Meal\Model\Breakfast;
    use App\Meal\Model\Lunch;
    use App\Meal\Model\Dinner;

    class MealController 
    {
        public function index()
        {
            $currentHour = (int) date('H');
            
            if ($currentHour < 12) {
                $meal = new Breakfast();
            } elseif ($currentHour < 16) {
                $meal = new Lunch();
            } else {
                $meal = new Dinner();
            }

            echo $meal->getName();
        }
    }


Ici, le Controller doit avoir connaissance des différentes classes pour faire son affichage. Ce qui pose un problème dans le cas d'une modification des types de repas : il faut modifier l'ensemble des fichiers qui manipulent les classes concrètes (là où il y a des 'use').

Le but de la Factory est d'éviter cela afin que le Controller n'ait plus à gérer les différents modèles. Tout ce qu'il doit savoir est qu'il manipule une instance qui implémente l'interface MealInterface. Cette interface a une méthode getName(), commune à toutes les classes qui l'implémentent.

3.5 Avec l'Abstract Factory

Voici une implémentation possible de la classe MealFactory, elle reprend la logique du MealController :

MealFactory.php
<?php
    namespace App\Meal\Factory;

    use App\Meal\Model\MealInterface;
    use App\Meal\Model\Breakfast;
    use App\Meal\Model\Lunch;
    use App\Meal\Model\Dinner;

    class MealFactory implements MealFactoryInterface
    {
        public static function create(int $hour): MealInterface
        {
            if ($hour < 12) {
                $meal = new Breakfast();
            } elseif ($hour < 16) {
                $meal = new Lunch();
            } else {
                $meal = new Dinner();
            }

            return $meal;
        }
    }


Elle implémente l'interface MealFactoryInterface. Cette interface permettra dans le cas où l'on souhaite créer une deuxième MealFactory (par exemple pour un pays où les repas seraient différents) de mettre en place le mécanisme d'AbstractFactory (ie. plusieurs classes de type MealFactory (FrenchMealFactory/AfricanMealFactory)).

MealFactory.php
<?php
    namespace App\Meal\Factory;

    use App\Meal\Model\MealInterface;

    interface MealFactoryInterface
    {
        public function create(int $hour): MealInterface;
    }


Voici désormais ce que fera le MealController :

MealController.php
<?php 
    namespace App\Meal\Controller;

    use App\Meal\Factory\MealFactory;

    class MealController
    {
        public function index()
        {
            $mealFactory = MealFactory::create((int) date('H'));
            echo $meal->getName();
        }
    }


🧙‍♂️️L'usage des méthodes static est assez déconseillée car le Controller a ici une dépendance avec la classe concrète de la Factory. Si on souhaite utiliser une autre MealFactory, il faudra modifier dans l'ensemble du code cet usage. Mieux vaut que MealFactory implémente l'interface MealFactoryInterface. On injectera ensuite la Factory dans le constructeur des classes qui en dépendent.



4. Sources

📖️️https://designpatternsphp.readthedocs.io/en/latest/Creational/StaticFactory/README.html

📖️️https://waytolearnx.com/2020/02/design-patterns-static-factory-en-php.html